package com.bangcommunity.bbframe.fdfs.connection;

import com.bangcommunity.bbframe.fdfs.FdfsSourceInputStream;
import com.bangcommunity.bbframe.fdfs.StorageAddress;
import org.csource.common.Base64;
import org.csource.common.MyException;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.DownloadCallback;
import org.csource.fastdfs.FileInfo;
import org.csource.fastdfs.ProtoCommon;
import org.csource.fastdfs.UploadCallback;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;

/**
 * @author tanghc
 * @date 18/6/22
 */
public class StorageConnectionImpl extends BaseFdfsSocketConnection implements StorageConnection {
    private int storePathIndex;
    public final static Base64 base64 = new Base64('-', '_', '.', 0);

    public StorageConnectionImpl(String address) {
        super(address);
        this.storePathIndex = 0;
    }

    public StorageConnectionImpl(StorageAddress storageAddress) {
        super(storageAddress.getHost(), storageAddress.getPort());
        this.storePathIndex = storageAddress.getStorePathIndex();
    }

    public int getStorePathIndex() {
        return storePathIndex;
    }

    public void setStorePathIndex(int storePathIndex) {
        this.storePathIndex = storePathIndex;
    }

    /**
     * upload file to storage server
     *
     * @param cmd
     *            the command code
     * @param groupName
     *            the group name to upload file to, can be empty
     * @param masterFilename
     *            the master file name to generate the slave file
     * @param prefixName
     *            the prefix name to generate the slave file
     * @param fileExtName
     *            file ext name, do not include dot(.)
     * @param fileSize
     *            the file size
     * @param callback
     *            the write data callback object
     * @return 2 elements string array if success:<br>
     *         <ul>
     *         <li>results[0]: the group name to store the file</li>
     *         </ul>
     *         <ul>
     *         <li>results[1]: the new created filename</li>
     *         </ul>
     *         return null if fail
     */
    @Override
    public String[] doUploadFile(byte cmd, String groupName, String masterFilename, String prefixName,
            String fileExtName, long fileSize, UploadCallback callback) throws IOException, MyException {
        byte[] header;
        byte[] ext_name_bs;
        String new_group_name;
        String remote_filename;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] sizeBytes;
        byte[] hexLenBytes;
        byte[] masterFilenameBytes;
        boolean bUploadSlave;
        int offset;
        long body_len;

        bUploadSlave = ((groupName != null && groupName.length() > 0)
                && (masterFilename != null && masterFilename.length() > 0) && (prefixName != null));

        storageSocket = this.getSocket();

        ext_name_bs = new byte[ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN];
        Arrays.fill(ext_name_bs, (byte) 0);
        if (fileExtName != null && fileExtName.length() > 0) {
            byte[] bs = fileExtName.getBytes(ClientGlobal.g_charset);
            int ext_name_len = bs.length;
            if (ext_name_len > ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN) {
                ext_name_len = ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN;
            }
            System.arraycopy(bs, 0, ext_name_bs, 0, ext_name_len);
        }

        if (bUploadSlave) {
            masterFilenameBytes = masterFilename.getBytes(ClientGlobal.g_charset);

            sizeBytes = new byte[2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE];
            body_len = sizeBytes.length + ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN + ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN
                    + masterFilenameBytes.length + fileSize;

            hexLenBytes = ProtoCommon.long2buff(masterFilename.length());
            System.arraycopy(hexLenBytes, 0, sizeBytes, 0, hexLenBytes.length);
            offset = hexLenBytes.length;
        } else {
            masterFilenameBytes = null;
            sizeBytes = new byte[1 + 1 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE];
            body_len = sizeBytes.length + ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN + fileSize;

            sizeBytes[0] = (byte) this.storePathIndex;
            offset = 1;
        }

        hexLenBytes = ProtoCommon.long2buff(fileSize);
        System.arraycopy(hexLenBytes, 0, sizeBytes, offset, hexLenBytes.length);

        OutputStream out = storageSocket.getOutputStream();
        header = ProtoCommon.packHeader(cmd, body_len, (byte) 0);
        byte[] wholePkg = new byte[(int) (header.length + body_len - fileSize)];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        System.arraycopy(sizeBytes, 0, wholePkg, header.length, sizeBytes.length);
        offset = header.length + sizeBytes.length;
        if (bUploadSlave) {
            byte[] prefix_name_bs = new byte[ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN];
            byte[] bs = prefixName.getBytes(ClientGlobal.g_charset);
            int prefix_name_len = bs.length;
            Arrays.fill(prefix_name_bs, (byte) 0);
            if (prefix_name_len > ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN) {
                prefix_name_len = ProtoCommon.FDFS_FILE_PREFIX_MAX_LEN;
            }
            if (prefix_name_len > 0) {
                System.arraycopy(bs, 0, prefix_name_bs, 0, prefix_name_len);
            }

            System.arraycopy(prefix_name_bs, 0, wholePkg, offset, prefix_name_bs.length);
            offset += prefix_name_bs.length;
        }

        System.arraycopy(ext_name_bs, 0, wholePkg, offset, ext_name_bs.length);
        offset += ext_name_bs.length;

        if (bUploadSlave) {
            System.arraycopy(masterFilenameBytes, 0, wholePkg, offset, masterFilenameBytes.length);
            offset += masterFilenameBytes.length;
        }

        out.write(wholePkg);

        if ((this.errno = (byte) callback.send(out)) != 0) {
            return null;
        }

        ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);
        this.errno = pkgInfo.errno;
        if (pkgInfo.errno != 0) {
            return null;
        }

        if (pkgInfo.body.length <= ProtoCommon.FDFS_GROUP_NAME_MAX_LEN) {
            throw new MyException("body length: " + pkgInfo.body.length + " <= " + ProtoCommon.FDFS_GROUP_NAME_MAX_LEN);
        }

        new_group_name = new String(pkgInfo.body, 0, ProtoCommon.FDFS_GROUP_NAME_MAX_LEN).trim();
        remote_filename = new String(pkgInfo.body, ProtoCommon.FDFS_GROUP_NAME_MAX_LEN,
                pkgInfo.body.length - ProtoCommon.FDFS_GROUP_NAME_MAX_LEN);
        String[] results = new String[2];
        results[0] = new_group_name;
        results[1] = remote_filename;

        return results;
    }

    /**
     * modify appender file to storage server
     *
     * @param groupName
     *            the group name of appender file
     * @param fileId
     *            the appender filename
     * @param fileOffset
     *            the offset of appender file
     * @param modifySize
     *            the modify size
     * @param callback
     *            the write data callback object
     * @return 0 for success, other for fail
     */
    @Override
    public int doModifyFile(String groupName, String fileId, long fileOffset, long modifySize, UploadCallback callback)
            throws IOException, MyException {
        byte[] header;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] hexLenBytes;
        byte[] appenderFilenameBytes;
        int offset;
        long body_len;

        if ((groupName == null || groupName.length() == 0) || (fileId == null || fileId.length() == 0)) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return this.errno;
        }

        storageSocket = this.getSocket();

        appenderFilenameBytes = fileId.getBytes(ClientGlobal.g_charset);
        body_len = 3 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + appenderFilenameBytes.length + modifySize;

        header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_MODIFY_FILE, body_len, (byte) 0);
        byte[] wholePkg = new byte[(int) (header.length + body_len - modifySize)];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        offset = header.length;

        hexLenBytes = ProtoCommon.long2buff(fileId.length());
        System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
        offset += hexLenBytes.length;

        hexLenBytes = ProtoCommon.long2buff(fileOffset);
        System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
        offset += hexLenBytes.length;

        hexLenBytes = ProtoCommon.long2buff(modifySize);
        System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
        offset += hexLenBytes.length;

        OutputStream out = storageSocket.getOutputStream();

        System.arraycopy(appenderFilenameBytes, 0, wholePkg, offset, appenderFilenameBytes.length);
        offset += appenderFilenameBytes.length;

        out.write(wholePkg);
        if ((this.errno = (byte) callback.send(out)) != 0) {
            return this.errno;
        }

        ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);
        this.errno = pkgInfo.errno;
        if (pkgInfo.errno != 0) {
            return this.errno;
        }

        return 0;
    }

    /**
     * append file to storage server
     *
     * @param groupName
     *            the group name of appender file
     * @param fileId
     *            the appender filename
     * @param fileSize
     *            the file size
     * @param callback
     *            the write data callback object
     * @return 0 for success, other for fail
     */
    @Override
    public int doAppendFile(String groupName, String fileId, long fileSize, UploadCallback callback)
            throws IOException, MyException {
        byte[] header;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] hexLenBytes;
        byte[] appenderFilenameBytes;
        int offset;
        long body_len;

        if ((groupName == null || groupName.length() == 0) || (fileId == null || fileId.length() == 0)) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return this.errno;
        }

        storageSocket = this.getSocket();

        appenderFilenameBytes = fileId.getBytes(ClientGlobal.g_charset);
        body_len = 2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + appenderFilenameBytes.length + fileSize;

        header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_APPEND_FILE, body_len, (byte) 0);
        byte[] wholePkg = new byte[(int) (header.length + body_len - fileSize)];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        offset = header.length;

        hexLenBytes = ProtoCommon.long2buff(fileId.length());
        System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
        offset += hexLenBytes.length;

        hexLenBytes = ProtoCommon.long2buff(fileSize);
        System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
        offset += hexLenBytes.length;

        OutputStream out = storageSocket.getOutputStream();

        System.arraycopy(appenderFilenameBytes, 0, wholePkg, offset, appenderFilenameBytes.length);
        offset += appenderFilenameBytes.length;

        out.write(wholePkg);
        if ((this.errno = (byte) callback.send(out)) != 0) {
            return this.errno;
        }

        ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);
        this.errno = pkgInfo.errno;
        if (pkgInfo.errno != 0) {
            return this.errno;
        }

        return 0;
    }

    /**
     * send package to storage server
     *
     * @param cmd
     *            which command to send
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            filename on storage server
     */
    protected void sendPackage(byte cmd, String groupName, String fileId) throws IOException {
        byte[] header;
        byte[] groupBytes;
        byte[] filenameBytes;
        byte[] bs;
        int groupLen;

        groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
        bs = groupName.getBytes(ClientGlobal.g_charset);
        filenameBytes = fileId.getBytes(ClientGlobal.g_charset);

        Arrays.fill(groupBytes, (byte) 0);
        if (bs.length <= groupBytes.length) {
            groupLen = bs.length;
        } else {
            groupLen = groupBytes.length;
        }
        System.arraycopy(bs, 0, groupBytes, 0, groupLen);

        header = ProtoCommon.packHeader(cmd, groupBytes.length + filenameBytes.length, (byte) 0);
        byte[] wholePkg = new byte[header.length + groupBytes.length + filenameBytes.length];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        System.arraycopy(groupBytes, 0, wholePkg, header.length, groupBytes.length);
        System.arraycopy(filenameBytes, 0, wholePkg, header.length + groupBytes.length, filenameBytes.length);
        this.getSocket().getOutputStream().write(wholePkg);
    }

    /**
     * send package to storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            filename on storage server
     * @param fileOffset
     *            the start offset of the file
     * @param downloadBytes
     *            download bytes
     */
    protected void sendDownloadPackage(String groupName, String fileId, long fileOffset, long downloadBytes)
            throws IOException {
        byte[] header;
        byte[] bsOffset;
        byte[] bsDownBytes;
        byte[] groupBytes;
        byte[] filenameBytes;
        byte[] bs;
        int groupLen;

        bsOffset = ProtoCommon.long2buff(fileOffset);
        bsDownBytes = ProtoCommon.long2buff(downloadBytes);
        groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
        bs = groupName.getBytes(ClientGlobal.g_charset);
        filenameBytes = fileId.getBytes(ClientGlobal.g_charset);

        Arrays.fill(groupBytes, (byte) 0);
        if (bs.length <= groupBytes.length) {
            groupLen = bs.length;
        } else {
            groupLen = groupBytes.length;
        }
        System.arraycopy(bs, 0, groupBytes, 0, groupLen);

        header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_DOWNLOAD_FILE,
                bsOffset.length + bsDownBytes.length + groupBytes.length + filenameBytes.length, (byte) 0);
        byte[] wholePkg = new byte[header.length + bsOffset.length + bsDownBytes.length + groupBytes.length
                + filenameBytes.length];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        System.arraycopy(bsOffset, 0, wholePkg, header.length, bsOffset.length);
        System.arraycopy(bsDownBytes, 0, wholePkg, header.length + bsOffset.length, bsDownBytes.length);
        System.arraycopy(groupBytes, 0, wholePkg, header.length + bsOffset.length + bsDownBytes.length,
                groupBytes.length);
        System.arraycopy(filenameBytes, 0, wholePkg,
                header.length + bsOffset.length + bsDownBytes.length + groupBytes.length, filenameBytes.length);
        this.getSocket().getOutputStream().write(wholePkg);
    }

    /**
     * download file from storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            filename on storage server
     * @param fileOffset
     *            the start offset of the file
     * @param downloadBytes
     *            download bytes, 0 for remain bytes from offset
     * @return file content/buff, return null if fail
     */
    @Override
    public byte[] downloadFile(String groupName, String fileId, long fileOffset, long downloadBytes)
            throws IOException, MyException {
        Socket storageSocket = this.getSocket();

        ProtoCommon.RecvPackageInfo pkgInfo;

        this.sendDownloadPackage(groupName, fileId, fileOffset, downloadBytes);
        pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);

        this.errno = pkgInfo.errno;
        if (pkgInfo.errno != 0) {
            return null;
        }

        return pkgInfo.body;
    }

    /**
     * download file from storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param remoteFilename
     *            filename on storage server
     * @param fileOffset
     *            the start offset of the file
     * @param downloadBytes
     *            download bytes, 0 for remain bytes from offset
     * @param callback
     *            call callback.recv() when data arrive
     * @return 0 success, return none zero errno if fail
     */
    @Override
    public int downloadFile(String groupName, String remoteFilename, long fileOffset, long downloadBytes,
                            DownloadCallback callback) throws IOException, MyException {
        FdfsSourceInputStream fdfsSourceInputStream = null;
        try {
            fdfsSourceInputStream = getFdfsSourceInputStream(groupName, remoteFilename, fileOffset, downloadBytes);
            if(fdfsSourceInputStream == null){
                return -1;
            }
            byte[] buff = new byte[2 * 1024];
            long size = fdfsSourceInputStream.getSize();
            long remainBytes = size;
            int bytes;
            int result;
            while (remainBytes > 0) {
                if ((bytes = fdfsSourceInputStream.read(buff, 0, remainBytes > buff.length ? buff.length : (int) remainBytes)) < 0) {
                    throw new IOException(
                            "recv package size " + (size - remainBytes) + " != " + size);
                }

                if ((result = callback.recv(size, buff, bytes)) != 0) {
                    this.errno = (byte) result;
                    return result;
                }

                remainBytes -= bytes;
            }
        } finally {
        }

        return 0;
    }
    @Override
    public FdfsSourceInputStream getFdfsSourceInputStream(String groupName, String remoteFilename, long fileOffset, long downloadBytes) throws IOException, MyException {
        int result;
        Socket storageSocket = this.getSocket();

        ProtoCommon.RecvHeaderInfo header;
        this.sendDownloadPackage(groupName, remoteFilename, fileOffset, downloadBytes);

        InputStream in = storageSocket.getInputStream();
        header = ProtoCommon.recvHeader(in, ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);
        this.errno = header.errno;
        if (header.errno != 0) {
            return null;
        }
        return new FdfsSourceInputStream(in,header.body_len);
    }

    /**
     * truncate appender file from storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            the appender filename
     * @param truncatedFileSize
     *            truncated file size
     * @return 0 for success, none zero for fail (error code)
     */
    @Override
    public int truncateFile(String groupName, String fileId, long truncatedFileSize) throws IOException, MyException {
        byte[] header;
        boolean bNewConnection;
        Socket storageSocket;
        byte[] hexLenBytes;
        byte[] appenderFilenameBytes;
        int offset;
        int body_len;

        if ((groupName == null || groupName.length() == 0) || (fileId == null || fileId.length() == 0)) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return this.errno;
        }

        storageSocket = this.getSocket();

        appenderFilenameBytes = fileId.getBytes(ClientGlobal.g_charset);
        body_len = 2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + appenderFilenameBytes.length;

        header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_TRUNCATE_FILE, body_len, (byte) 0);
        byte[] wholePkg = new byte[header.length + body_len];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        offset = header.length;

        hexLenBytes = ProtoCommon.long2buff(fileId.length());
        System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
        offset += hexLenBytes.length;

        hexLenBytes = ProtoCommon.long2buff(truncatedFileSize);
        System.arraycopy(hexLenBytes, 0, wholePkg, offset, hexLenBytes.length);
        offset += hexLenBytes.length;

        OutputStream out = storageSocket.getOutputStream();

        System.arraycopy(appenderFilenameBytes, 0, wholePkg, offset, appenderFilenameBytes.length);
        offset += appenderFilenameBytes.length;

        out.write(wholePkg);
        ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);
        this.errno = pkgInfo.errno;
        return pkgInfo.errno;
    }

    /**
     * delete file from storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            filename on storage server
     * @return 0 for success, none zero for fail (error code)
     */
    @Override
    public int deleteFile(String groupName, String fileId) throws IOException, MyException {
        Socket storageSocket = this.getSocket();

        this.sendPackage(ProtoCommon.STORAGE_PROTO_CMD_DELETE_FILE, groupName, fileId);
        ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(),
                ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);

        this.errno = pkgInfo.errno;
        return pkgInfo.errno;
    }

    /**
     * get all metadata items from storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            filename on storage server
     * @return meta info array, return null if fail
     */
    @Override
    public NameValuePair[] getMetadata(String groupName, String fileId) throws IOException, MyException {
        Socket storageSocket = this.getSocket();

        ProtoCommon.RecvPackageInfo pkgInfo;

        this.sendPackage(ProtoCommon.STORAGE_PROTO_CMD_GET_METADATA, groupName, fileId);
        pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP, -1);

        this.errno = pkgInfo.errno;
        if (pkgInfo.errno != 0) {
            return null;
        }

        return ProtoCommon.split_metadata(new String(pkgInfo.body, ClientGlobal.g_charset));
    }

    /**
     * set metadata items to storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            filename on storage server
     * @param metaList
     *            meta item array
     * @param opFlag
     *            flag, can be one of following values: <br>
     *            <ul>
     *            <li>ProtoCommon.STORAGE_SET_METADATA_FLAG_OVERWRITE: overwrite all old metadata
     *            items</li>
     *            </ul>
     *            <ul>
     *            <li>ProtoCommon.STORAGE_SET_METADATA_FLAG_MERGE: merge, insert when the metadata
     *            item not exist, otherwise update it</li>
     *            </ul>
     * @return 0 for success, !=0 fail (error code)
     */
    @Override
    public int setMetadata(String groupName, String fileId, NameValuePair[] metaList, byte opFlag)
            throws IOException, MyException {
        Socket storageSocket = this.getSocket();

        byte[] header;
        byte[] groupBytes;
        byte[] filenameBytes;
        byte[] meta_buff;
        byte[] bs;
        int groupLen;
        byte[] sizeBytes;
        ProtoCommon.RecvPackageInfo pkgInfo;

        if (metaList == null) {
            meta_buff = new byte[0];
        } else {
            meta_buff = ProtoCommon.pack_metadata(metaList).getBytes(ClientGlobal.g_charset);
        }

        filenameBytes = fileId.getBytes(ClientGlobal.g_charset);
        sizeBytes = new byte[2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE];
        Arrays.fill(sizeBytes, (byte) 0);

        bs = ProtoCommon.long2buff(filenameBytes.length);
        System.arraycopy(bs, 0, sizeBytes, 0, bs.length);
        bs = ProtoCommon.long2buff(meta_buff.length);
        System.arraycopy(bs, 0, sizeBytes, ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE, bs.length);

        groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
        bs = groupName.getBytes(ClientGlobal.g_charset);

        Arrays.fill(groupBytes, (byte) 0);
        if (bs.length <= groupBytes.length) {
            groupLen = bs.length;
        } else {
            groupLen = groupBytes.length;
        }
        System.arraycopy(bs, 0, groupBytes, 0, groupLen);

        header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_SET_METADATA,
                2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + 1 + groupBytes.length + filenameBytes.length
                        + meta_buff.length,
                (byte) 0);
        OutputStream out = storageSocket.getOutputStream();
        byte[] wholePkg = new byte[header.length + sizeBytes.length + 1 + groupBytes.length + filenameBytes.length];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        System.arraycopy(sizeBytes, 0, wholePkg, header.length, sizeBytes.length);
        wholePkg[header.length + sizeBytes.length] = opFlag;
        System.arraycopy(groupBytes, 0, wholePkg, header.length + sizeBytes.length + 1, groupBytes.length);
        System.arraycopy(filenameBytes, 0, wholePkg, header.length + sizeBytes.length + 1 + groupBytes.length,
                filenameBytes.length);
        out.write(wholePkg);
        if (meta_buff.length > 0) {
            out.write(meta_buff);
        }

        pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP, 0);

        this.errno = pkgInfo.errno;
        return pkgInfo.errno;
    }

    /**
     * get file info decoded from the filename, fetch from the storage if necessary
     *
     * @param groupName
     *            the group name
     * @param fileId
     *            the filename
     * @return FileInfo object for success, return null for fail
     */
    @Override
    public FileInfo getFileInfo(String groupName, String fileId) throws IOException, MyException {
        if (fileId.length() < ProtoCommon.FDFS_FILE_PATH_LEN + ProtoCommon.FDFS_FILENAME_BASE64_LENGTH
                + ProtoCommon.FDFS_FILE_EXT_NAME_MAX_LEN + 1) {
            this.errno = ProtoCommon.ERR_NO_EINVAL;
            return null;
        }

        byte[] buff = base64.decodeAuto(fileId.substring(ProtoCommon.FDFS_FILE_PATH_LEN,
                ProtoCommon.FDFS_FILE_PATH_LEN + ProtoCommon.FDFS_FILENAME_BASE64_LENGTH));

        long file_size = ProtoCommon.buff2long(buff, 4 * 2);
        if (((fileId.length() > ProtoCommon.TRUNK_LOGIC_FILENAME_LENGTH)
                || ((fileId.length() > ProtoCommon.NORMAL_LOGIC_FILENAME_LENGTH)
                        && ((file_size & ProtoCommon.TRUNK_FILE_MARK_SIZE) == 0)))
                || ((file_size & ProtoCommon.APPENDER_FILE_SIZE) != 0)) { // slave file or appender
                                                                          // file
            FileInfo fi = this.queryFileInfo(groupName, fileId);
            if (fi == null) {
                return null;
            }
            return fi;
        }

        FileInfo fileInfo = new FileInfo(file_size, 0, 0, ProtoCommon.getIpAddress(buff, 0));
        fileInfo.setCreateTimestamp(ProtoCommon.buff2int(buff, 4));
        if ((file_size >> 63) != 0) {
            file_size &= 0xFFFFFFFFL; // low 32 bits is file size
            fileInfo.setFileSize(file_size);
        }
        fileInfo.setCrc32(ProtoCommon.buff2int(buff, 4 * 4));

        return fileInfo;
    }

    /**
     * get file info from storage server
     *
     * @param groupName
     *            the group name of storage server
     * @param fileId
     *            filename on storage server
     * @return FileInfo object for success, return null for fail
     */
    @Override
    public FileInfo queryFileInfo(String groupName, String fileId) throws IOException, MyException {
        Socket storageSocket = this.getSocket();

        byte[] header;
        byte[] groupBytes;
        byte[] filenameBytes;
        byte[] bs;
        int groupLen;
        ProtoCommon.RecvPackageInfo pkgInfo;

        filenameBytes = fileId.getBytes(ClientGlobal.g_charset);
        groupBytes = new byte[ProtoCommon.FDFS_GROUP_NAME_MAX_LEN];
        bs = groupName.getBytes(ClientGlobal.g_charset);

        Arrays.fill(groupBytes, (byte) 0);
        if (bs.length <= groupBytes.length) {
            groupLen = bs.length;
        } else {
            groupLen = groupBytes.length;
        }
        System.arraycopy(bs, 0, groupBytes, 0, groupLen);

        header = ProtoCommon.packHeader(ProtoCommon.STORAGE_PROTO_CMD_QUERY_FILE_INFO,
                +groupBytes.length + filenameBytes.length, (byte) 0);
        OutputStream out = storageSocket.getOutputStream();
        byte[] wholePkg = new byte[header.length + groupBytes.length + filenameBytes.length];
        System.arraycopy(header, 0, wholePkg, 0, header.length);
        System.arraycopy(groupBytes, 0, wholePkg, header.length, groupBytes.length);
        System.arraycopy(filenameBytes, 0, wholePkg, header.length + groupBytes.length, filenameBytes.length);
        out.write(wholePkg);

        pkgInfo = ProtoCommon.recvPackage(storageSocket.getInputStream(), ProtoCommon.STORAGE_PROTO_CMD_RESP,
                3 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE + ProtoCommon.FDFS_IPADDR_SIZE);

        this.errno = pkgInfo.errno;
        if (pkgInfo.errno != 0) {
            return null;
        }

        long file_size = ProtoCommon.buff2long(pkgInfo.body, 0);
        int create_timestamp = (int) ProtoCommon.buff2long(pkgInfo.body, ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE);
        int crc32 = (int) ProtoCommon.buff2long(pkgInfo.body, 2 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE);
        String source_ip_addr = (new String(pkgInfo.body, 3 * ProtoCommon.FDFS_PROTO_PKG_LEN_SIZE,
                ProtoCommon.FDFS_IPADDR_SIZE)).trim();
        return new FileInfo(file_size, create_timestamp, crc32, source_ip_addr);
    }
}
